// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library world_test;

import 'package:compiler/src/elements/names.dart';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/common/names.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/js_model/js_world.dart' show JClosedWorld;
import 'package:compiler/src/universe/class_hierarchy.dart';
import 'package:compiler/src/universe/class_set.dart';
import '../helpers/type_test_helper.dart';

void main() {
  runTests() async {
    await testClassSets();
    await testProperties();
    await testNativeClasses();
    await testCommonSubclasses();
    await testLiveMembers();
  }

  asyncTest(() async {
    print('--test from kernel------------------------------------------------');
    await runTests();
  });
}

testClassSets() async {
  var env = await TypeEnvironment.create(r"""
      import 'dart:html' as html;

      mixin class A implements X {}
      mixin class B {}
      class C_Super extends A {}
      class C extends C_Super {}
      class D implements A {}
      class E extends B implements A {}
      class F extends Object with A implements B {}
      class G extends Object with B, A {}
      class X {}

      main() {
        A();
        B();
        C();
        D();
        E();
        F();
        G();
        html.window;
        html.Worker('');
      }
      """, testBackendWorld: true);
  JClosedWorld closedWorld = env.jClosedWorld;
  ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;

  final Object_ = env.getElement("Object") as ClassEntity;
  final A = env.getElement("A") as ClassEntity;
  final B = env.getElement("B") as ClassEntity;
  final C = env.getElement("C") as ClassEntity;
  final D = env.getElement("D") as ClassEntity;
  final E = env.getElement("E") as ClassEntity;
  final F = env.getElement("F") as ClassEntity;
  final G = env.getElement("G") as ClassEntity;
  final X = env.getElement("X") as ClassEntity;

  void checkClasses(
    String property,
    ClassEntity cls,
    Iterable<ClassEntity> foundClasses,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
  }) {
    for (ClassEntity expectedClass in expectedClasses) {
      Expect.isTrue(
        foundClasses.contains(expectedClass),
        "Expect $expectedClass in '$property' on $cls. "
        "Found:\n ${foundClasses.join('\n ')}\n"
        "${closedWorld.classHierarchy.dump(cls)}",
      );
    }
    if (exact) {
      Expect.equals(
        expectedClasses.length,
        foundClasses.length,
        "Unexpected classes "
        "${foundClasses.where((c) => !expectedClasses.contains(c))} "
        "in '$property' on $cls.\n"
        "${closedWorld.classHierarchy.dump(cls)}",
      );
    }
  }

  void check(
    String property,
    ClassEntity cls,
    Iterable<ClassEntity> foundClasses,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
    void forEach(ClassEntity cls, ForEachFunction f)?,
    int getCount(ClassEntity cls)?,
  }) {
    checkClasses(property, cls, foundClasses, expectedClasses, exact: exact);

    if (forEach != null) {
      List<ClassEntity> visited = <ClassEntity>[];
      forEach(cls, (ClassEntity c) {
        visited.add(c);
        return IterationStep.continue_;
      });
      checkClasses(
        'forEach($property)',
        cls,
        visited,
        expectedClasses,
        exact: exact,
      );
    }

    if (getCount != null && exact) {
      int count = getCount(cls);
      Expect.equals(
        expectedClasses.length,
        count,
        "Unexpected class count in '$property' on $cls.\n"
        "${closedWorld.classHierarchy.dump(cls)}",
      );
    }
  }

  void testSubclasses(
    ClassEntity cls,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
  }) {
    check(
      'subclassesOf',
      cls,
      closedWorld.classHierarchy.subclassesOf(cls),
      expectedClasses,
      exact: exact,
    );
  }

  void testStrictSubclasses(
    ClassEntity cls,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
  }) {
    check(
      'strictSubclassesOf',
      cls,
      closedWorld.classHierarchy.strictSubclassesOf(cls),
      expectedClasses,
      exact: exact,
      forEach: closedWorld.classHierarchy.forEachStrictSubclassOf,
      getCount: closedWorld.classHierarchy.strictSubclassCount,
    );
  }

  void testStrictSubtypes(
    ClassEntity cls,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
  }) {
    check(
      'strictSubtypesOf',
      cls,
      closedWorld.classHierarchy.strictSubtypesOf(cls),
      expectedClasses,
      exact: exact,
      forEach: closedWorld.classHierarchy.forEachStrictSubtypeOf,
      getCount: closedWorld.classHierarchy.strictSubtypeCount,
    );
  }

  void testMixinUses(
    ClassEntity cls,
    List<ClassEntity> expectedClasses, {
    bool exact = true,
  }) {
    check(
      'mixinUsesOf',
      cls,
      closedWorld.mixinUsesOf(cls),
      expectedClasses,
      exact: exact,
    );
  }

  testSubclasses(Object_, [A, B, C, D, E, F, G], exact: false);
  testSubclasses(A, [A, C]);
  testSubclasses(B, [B, E]);
  testSubclasses(C, [C]);
  testSubclasses(D, [D]);
  testSubclasses(E, [E]);
  testSubclasses(F, [F]);
  testSubclasses(G, [G]);
  testSubclasses(X, []);

  testStrictSubclasses(Object_, [A, B, C, D, E, F, G], exact: false);
  testStrictSubclasses(A, [C]);
  testStrictSubclasses(B, [E]);
  testStrictSubclasses(C, []);
  testStrictSubclasses(D, []);
  testStrictSubclasses(E, []);
  testStrictSubclasses(F, []);
  testStrictSubclasses(G, []);
  testStrictSubclasses(X, []);

  testStrictSubtypes(Object_, [A, B, C, D, E, F, G], exact: false);
  testStrictSubtypes(A, [C, D, E, F, G]);
  testStrictSubtypes(B, [E, F, G]);
  testStrictSubtypes(C, []);
  testStrictSubtypes(D, []);
  testStrictSubtypes(E, []);
  testStrictSubtypes(F, []);
  testStrictSubtypes(G, []);
  testStrictSubtypes(X, [A, C, D, E, F, G]);

  testMixinUses(Object_, []);
  testMixinUses(A, [
    elementEnvironment.getSuperClass(F)!,
    elementEnvironment.getSuperClass(G)!,
  ]);
  testMixinUses(B, [
    elementEnvironment.getSuperClass(elementEnvironment.getSuperClass(G)!)!,
  ]);
  testMixinUses(C, []);
  testMixinUses(D, []);
  testMixinUses(E, []);
  testMixinUses(F, []);
  testMixinUses(G, []);
  testMixinUses(X, []);
}

testProperties() async {
  var env = await TypeEnvironment.create(r"""
      mixin class A {}
      class A1 extends A {}
      class A2 implements A {}
      class A3 extends Object with A {}

      mixin class B {}
      class B1 extends B {}
      class B2 implements B {}
      class B3 extends Object with B {}

      mixin class C {}
      class C1 extends C {}
      class C2 implements C {}
      class C3 extends Object with C {}

      mixin class D {}
      class D1 extends D {}
      class D2 implements D {}
      class D3 extends Object with D {}

      mixin class E {}
      class E1 extends E {}
      class E2 implements E {}
      class E3 extends Object with E {}

      mixin class F {}
      class F1 extends F {}
      class F2 implements F {}
      class F3 extends Object with F {}

      mixin class G {}
      class G1 extends G {}
      class G2 extends G1 {}
      class G3 extends G2 implements G {}
      class G4 extends G2 with G {}

      mixin class H {}
      class H1 extends H {}
      class H2 extends H1 {}
      class H3 extends H2 implements H {}
      class H4 extends H2 with H {}

      main() {
        B();
        C1();
        D2();
        E3();
        F1();
        F2();
        G2();
        G3();
        H4();
      }
      """, testBackendWorld: true);
  JClosedWorld closedWorld = env.jClosedWorld;

  check(String name, {bool? hasStrictSubtype, bool? hasOnlySubclasses}) {
    final cls = env.getElement(name) as ClassEntity;
    Expect.equals(
      hasStrictSubtype,
      closedWorld.classHierarchy.hasAnyStrictSubtype(cls),
      "Unexpected hasAnyStrictSubtype property on $cls.",
    );
    Expect.equals(
      hasOnlySubclasses,
      closedWorld.classHierarchy.hasOnlySubclasses(cls),
      "Unexpected hasOnlySubclasses property on $cls.",
    );
  }

  check("Object", hasStrictSubtype: true, hasOnlySubclasses: true);

  // No instantiated Ax classes.
  check("A", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("A1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("A2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("A3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class B instantiated
  check("B", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("B1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("B2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("B3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class C1 extends C instantiated
  check("C", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("C1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("C2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("C3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class D2 implements D instantiated
  check("D", hasStrictSubtype: true, hasOnlySubclasses: false);
  check("D1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("D2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("D3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class E2 extends Object with E instantiated
  check("E", hasStrictSubtype: true, hasOnlySubclasses: false);
  check("E1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("E2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("E3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class F1 extends F instantiated
  // class F2 implements F instantiated
  check("F", hasStrictSubtype: true, hasOnlySubclasses: false);
  check("F1", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("F2", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("F3", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class G2 extends G1 extends G instantiated
  // class G3 extends G2 extends G1 extends G instantiated
  check("G", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("G1", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("G2", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("G3", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("G4", hasStrictSubtype: false, hasOnlySubclasses: true);

  // class H4 extends H2 with H extends H1 extends H instantiated
  check("H", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("H1", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("H2", hasStrictSubtype: true, hasOnlySubclasses: true);
  check("H3", hasStrictSubtype: false, hasOnlySubclasses: true);
  check("H4", hasStrictSubtype: false, hasOnlySubclasses: true);
}

testNativeClasses() async {
  var env = await TypeEnvironment.create(r"""
      import 'dart:html' as html;
      main() {
        html.window; // Creates 'Window'.
        html.Worker(''); // Creates 'Worker'.
        html.CanvasElement() // Creates CanvasElement
            ..getContext(''); // Creates CanvasRenderingContext2D
      }
      """, testBackendWorld: true);
  JClosedWorld closedWorld = env.jClosedWorld;
  ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;
  LibraryEntity dart_html = elementEnvironment.lookupLibrary(Uris.dartHtml)!;

  ClassEntity clsEventTarget = elementEnvironment.lookupClass(
    dart_html,
    'EventTarget',
  )!;
  ClassEntity clsWindow = elementEnvironment.lookupClass(dart_html, 'Window')!;
  ClassEntity clsAbstractWorker = elementEnvironment.lookupClass(
    dart_html,
    'AbstractWorker',
  )!;
  ClassEntity clsWorker = elementEnvironment.lookupClass(dart_html, 'Worker')!;
  ClassEntity clsCanvasElement = elementEnvironment.lookupClass(
    dart_html,
    'CanvasElement',
  )!;
  ClassEntity clsCanvasRenderingContext = elementEnvironment.lookupClass(
    dart_html,
    'CanvasRenderingContext',
  )!;
  ClassEntity clsCanvasRenderingContext2D = elementEnvironment.lookupClass(
    dart_html,
    'CanvasRenderingContext2D',
  )!;

  List<ClassEntity> allClasses = [
    clsEventTarget,
    clsWindow,
    clsAbstractWorker,
    clsWorker,
    clsCanvasElement,
    clsCanvasRenderingContext,
    clsCanvasRenderingContext2D,
  ];

  check(
    ClassEntity cls, {
    required bool isDirectlyInstantiated,
    required bool isAbstractlyInstantiated,
    required bool isIndirectlyInstantiated,
    required bool hasStrictSubtype,
    required bool hasOnlySubclasses,
    ClassEntity? lubOfInstantiatedSubclasses,
    ClassEntity? lubOfInstantiatedSubtypes,
    int? instantiatedSubclassCount,
    int? instantiatedSubtypeCount,
    List<ClassEntity> subclasses = const <ClassEntity>[],
    List<ClassEntity> subtypes = const <ClassEntity>[],
  }) {
    ClassSet classSet = closedWorld.classHierarchy.getClassSet(cls);
    ClassHierarchyNode node = classSet.node;

    String dumpText = '\n${closedWorld.classHierarchy.dump(cls)}';

    Expect.equals(
      isDirectlyInstantiated,
      closedWorld.classHierarchy.isDirectlyInstantiated(cls),
      "Unexpected isDirectlyInstantiated property on $cls.$dumpText",
    );
    Expect.equals(
      isAbstractlyInstantiated,
      closedWorld.classHierarchy.isAbstractlyInstantiated(cls),
      "Unexpected isAbstractlyInstantiated property on $cls.$dumpText",
    );
    Expect.equals(
      isIndirectlyInstantiated,
      closedWorld.classHierarchy.isIndirectlyInstantiated(cls),
      "Unexpected isIndirectlyInstantiated property on $cls.$dumpText",
    );
    Expect.equals(
      hasStrictSubtype,
      closedWorld.classHierarchy.hasAnyStrictSubtype(cls),
      "Unexpected hasAnyStrictSubtype property on $cls.$dumpText",
    );
    Expect.equals(
      hasOnlySubclasses,
      closedWorld.classHierarchy.hasOnlySubclasses(cls),
      "Unexpected hasOnlySubclasses property on $cls.$dumpText",
    );
    Expect.equals(
      lubOfInstantiatedSubclasses,
      node.getLubOfInstantiatedSubclasses(),
      "Unexpected getLubOfInstantiatedSubclasses() result on $cls.$dumpText",
    );
    Expect.equals(
      lubOfInstantiatedSubtypes,
      classSet.getLubOfInstantiatedSubtypes(),
      "Unexpected getLubOfInstantiatedSubtypes() result on $cls.$dumpText",
    );
    if (instantiatedSubclassCount != null) {
      Expect.equals(
        instantiatedSubclassCount,
        node.instantiatedSubclassCount,
        "Unexpected instantiatedSubclassCount property on $cls.$dumpText",
      );
    }
    if (instantiatedSubtypeCount != null) {
      Expect.equals(
        instantiatedSubtypeCount,
        classSet.instantiatedSubtypeCount,
        "Unexpected instantiatedSubtypeCount property on $cls.$dumpText",
      );
    }
    for (ClassEntity other in allClasses) {
      if (other == cls) continue;
      if (!closedWorld.classHierarchy.isExplicitlyInstantiated(other)) continue;
      Expect.equals(
        subclasses.contains(other),
        closedWorld.classHierarchy.isSubclassOf(other, cls),
        "Unexpected subclass relation between $other and $cls.",
      );
      Expect.equals(
        subtypes.contains(other),
        closedWorld.classHierarchy.isSubtypeOf(other, cls),
        "Unexpected subtype relation between $other and $cls.",
      );
    }

    Set<ClassEntity> strictSubclasses = Set<ClassEntity>();
    closedWorld.classHierarchy.forEachStrictSubclassOf(cls, (
      ClassEntity other,
    ) {
      if (allClasses.contains(other)) {
        strictSubclasses.add(other);
      }
      return IterationStep.continue_;
    });
    Expect.setEquals(
      subclasses,
      strictSubclasses,
      "Unexpected strict subclasses of $cls: ${strictSubclasses}.",
    );

    Set<ClassEntity> strictSubtypes = Set<ClassEntity>();
    closedWorld.classHierarchy.forEachStrictSubtypeOf(cls, (ClassEntity other) {
      if (allClasses.contains(other)) {
        strictSubtypes.add(other);
      }
      return IterationStep.continue_;
    });
    Expect.setEquals(
      subtypes,
      strictSubtypes,
      "Unexpected strict subtypes of $cls: $strictSubtypes.",
    );
  }

  // Extended by Window.
  check(
    clsEventTarget,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: false,
    isIndirectlyInstantiated: true,
    hasStrictSubtype: true,
    hasOnlySubclasses: true,
    lubOfInstantiatedSubclasses: clsEventTarget,
    lubOfInstantiatedSubtypes: clsEventTarget,
    // May vary with implementation, do no test.
    instantiatedSubclassCount: null,
    instantiatedSubtypeCount: null,
    subclasses: [clsWindow, clsCanvasElement, clsWorker],
    subtypes: [clsWindow, clsCanvasElement, clsWorker],
  );

  // Created by 'html.window'.
  check(
    clsWindow,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: true,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: false,
    hasOnlySubclasses: true,
    lubOfInstantiatedSubclasses: clsWindow,
    lubOfInstantiatedSubtypes: clsWindow,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 0,
  );

  // Implemented by 'Worker'.
  check(
    clsAbstractWorker,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: false,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: true,
    hasOnlySubclasses: false,
    lubOfInstantiatedSubclasses: null,
    lubOfInstantiatedSubtypes: clsWorker,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 1,
    subtypes: [clsWorker],
  );

  // Created by 'new html.Worker'.
  check(
    clsWorker,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: true,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: false,
    hasOnlySubclasses: true,
    lubOfInstantiatedSubclasses: clsWorker,
    lubOfInstantiatedSubtypes: clsWorker,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 0,
  );

  // Created by 'new html.CanvasElement'.
  check(
    clsCanvasElement,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: true,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: false,
    hasOnlySubclasses: true,
    lubOfInstantiatedSubclasses: clsCanvasElement,
    lubOfInstantiatedSubtypes: clsCanvasElement,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 0,
  );

  // Implemented by CanvasRenderingContext2D and RenderingContext.
  check(
    clsCanvasRenderingContext,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: false,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: true,
    hasOnlySubclasses: false,
    lubOfInstantiatedSubclasses: null,
    lubOfInstantiatedSubtypes: clsCanvasRenderingContext,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 2,
    subtypes: [clsCanvasRenderingContext2D],
  );

  // Created by 'html.CanvasElement.getContext'.
  check(
    clsCanvasRenderingContext2D,
    isDirectlyInstantiated: false,
    isAbstractlyInstantiated: true,
    isIndirectlyInstantiated: false,
    hasStrictSubtype: false,
    hasOnlySubclasses: true,
    lubOfInstantiatedSubclasses: clsCanvasRenderingContext2D,
    lubOfInstantiatedSubtypes: clsCanvasRenderingContext2D,
    instantiatedSubclassCount: 0,
    instantiatedSubtypeCount: 0,
  );
}

testCommonSubclasses() async {
  var env = await TypeEnvironment.create(r"""
      class A {}
      class B {}
      class C extends A {}
      class D implements A {}
      class E extends B {}
      class F implements C, E {}
      class G extends C implements E {}
      class H implements C {}
      class I extends D implements E {}
      class J extends E implements D {}
      main() {
        A();
        B();
        C();
        D();
        E();
        F();
        G();
        H();
        I();
        J();
      }
      """, testBackendWorld: true);
  JClosedWorld closedWorld = env.jClosedWorld;

  final A = env.getElement("A") as ClassEntity;
  final B = env.getElement("B") as ClassEntity;
  final C = env.getElement("C") as ClassEntity;
  final F = env.getElement("F") as ClassEntity;
  final G = env.getElement("G") as ClassEntity;
  final I = env.getElement("I") as ClassEntity;
  final J = env.getElement("J") as ClassEntity;

  ClassQuery? toClassQuery(
    SubclassResult result,
    ClassEntity cls1,
    ClassQuery query1,
    ClassEntity cls2,
    ClassQuery query2,
  ) {
    switch (result) {
      case SimpleSubclassResult.empty:
        return null;
      case SimpleSubclassResult.exact1:
        return ClassQuery.exact;
      case SimpleSubclassResult.exact2:
        return ClassQuery.exact;
      case SimpleSubclassResult.subclass1:
        return ClassQuery.subclass;
      case SimpleSubclassResult.subclass2:
        return ClassQuery.subclass;
      case SimpleSubclassResult.subtype1:
        return ClassQuery.subtype;
      case SimpleSubclassResult.subtype2:
        return ClassQuery.subtype;
      case SetSubclassResult():
        return null;
    }
  }

  ClassEntity? toClassEntity(
    SubclassResult result,
    ClassEntity cls1,
    ClassQuery query1,
    ClassEntity cls2,
    ClassQuery query2,
  ) {
    switch (result) {
      case SimpleSubclassResult.empty:
        return null;
      case SimpleSubclassResult.exact1:
        return cls1;
      case SimpleSubclassResult.exact2:
        return cls2;
      case SimpleSubclassResult.subclass1:
        return cls1;
      case SimpleSubclassResult.subclass2:
        return cls2;
      case SimpleSubclassResult.subtype1:
        return cls1;
      case SimpleSubclassResult.subtype2:
        return cls2;
      case SetSubclassResult():
        return null;
    }
  }

  void check(
    ClassEntity cls1,
    ClassQuery query1,
    ClassEntity cls2,
    ClassQuery query2,
    SubclassResult expectedResult,
  ) {
    SubclassResult result1 = closedWorld.classHierarchy.commonSubclasses(
      cls1,
      query1,
      cls2,
      query2,
    );
    SubclassResult result2 = closedWorld.classHierarchy.commonSubclasses(
      cls2,
      query2,
      cls1,
      query1,
    );
    Expect.equals(
      toClassQuery(result1, cls1, query1, cls2, query2),
      toClassQuery(result2, cls2, query2, cls1, query1),
      "Asymmetric results for ($cls1,$query1) vs ($cls2,$query2):"
      "\n a vs b: $result1\n b vs a: $result2",
    );
    Expect.equals(
      toClassEntity(result1, cls1, query1, cls2, query2),
      toClassEntity(result2, cls2, query2, cls1, query1),
      "Asymmetric results for ($cls1,$query1) vs ($cls2,$query2):"
      "\n a vs b: $result1\n b vs a: $result2",
    );
    switch (expectedResult) {
      case SimpleSubclassResult():
        Expect.equals(
          expectedResult,
          result1,
          "Unexpected results for ($cls1,$query1) vs ($cls2,$query2):"
          "\n expected: $expectedResult\n actual: $result1",
        );
      case SetSubclassResult():
        Expect.type<SetSubclassResult>(result1);
        Expect.type<SetSubclassResult>(result2);
        result1 as SetSubclassResult;
        result2 as SetSubclassResult;
        Expect.setEquals(
          result1.classes,
          result2.classes,
          "Asymmetric results for ($cls1,$query1) vs ($cls2,$query2):"
          "\n a vs b: $result1\n b vs a: $result2",
        );
        Expect.setEquals(
          expectedResult.classes,
          result1.classes,
          "Unexpected results for ($cls1,$query1) vs ($cls2,$query2):"
          "\n expected: $expectedResult\n actual: $result1",
        );
    }
  }

  check(A, ClassQuery.exact, A, ClassQuery.exact, SimpleSubclassResult.exact1);
  check(
    A,
    ClassQuery.exact,
    A,
    ClassQuery.subclass,
    SimpleSubclassResult.exact1,
  );
  check(
    A,
    ClassQuery.exact,
    A,
    ClassQuery.subtype,
    SimpleSubclassResult.exact1,
  );
  check(
    A,
    ClassQuery.subclass,
    A,
    ClassQuery.subclass,
    SimpleSubclassResult.subclass1,
  );
  check(
    A,
    ClassQuery.subclass,
    A,
    ClassQuery.subtype,
    SimpleSubclassResult.subclass1,
  );
  check(
    A,
    ClassQuery.subtype,
    A,
    ClassQuery.subtype,
    SimpleSubclassResult.subtype1,
  );

  check(A, ClassQuery.exact, B, ClassQuery.exact, SimpleSubclassResult.empty);
  check(
    A,
    ClassQuery.exact,
    B,
    ClassQuery.subclass,
    SimpleSubclassResult.empty,
  );
  check(
    A,
    ClassQuery.subclass,
    B,
    ClassQuery.exact,
    SimpleSubclassResult.empty,
  );
  check(A, ClassQuery.exact, B, ClassQuery.subtype, SimpleSubclassResult.empty);
  check(A, ClassQuery.subtype, B, ClassQuery.exact, SimpleSubclassResult.empty);
  check(
    A,
    ClassQuery.subclass,
    B,
    ClassQuery.subclass,
    SimpleSubclassResult.empty,
  );
  check(A, ClassQuery.subclass, B, ClassQuery.subtype, SetSubclassResult([G]));
  check(A, ClassQuery.subtype, B, ClassQuery.subclass, SetSubclassResult([J]));
  check(
    A,
    ClassQuery.subtype,
    B,
    ClassQuery.subtype,
    SetSubclassResult([F, G, I, J]),
  );

  check(A, ClassQuery.exact, C, ClassQuery.exact, SimpleSubclassResult.empty);
  check(
    A,
    ClassQuery.exact,
    C,
    ClassQuery.subclass,
    SimpleSubclassResult.empty,
  );
  check(
    A,
    ClassQuery.subclass,
    C,
    ClassQuery.exact,
    SimpleSubclassResult.exact2,
  );
  check(A, ClassQuery.exact, C, ClassQuery.subtype, SimpleSubclassResult.empty);
  check(
    A,
    ClassQuery.subtype,
    C,
    ClassQuery.exact,
    SimpleSubclassResult.exact2,
  );
  check(
    A,
    ClassQuery.subclass,
    C,
    ClassQuery.subclass,
    SimpleSubclassResult.subclass2,
  );
  check(A, ClassQuery.subclass, C, ClassQuery.subtype, SetSubclassResult([C]));
  check(
    A,
    ClassQuery.subtype,
    C,
    ClassQuery.subclass,
    SimpleSubclassResult.subclass2,
  );
  check(
    A,
    ClassQuery.subtype,
    C,
    ClassQuery.subtype,
    SimpleSubclassResult.subtype2,
  );

  check(B, ClassQuery.exact, C, ClassQuery.exact, SimpleSubclassResult.empty);
  check(
    B,
    ClassQuery.exact,
    C,
    ClassQuery.subclass,
    SimpleSubclassResult.empty,
  );
  check(
    B,
    ClassQuery.subclass,
    C,
    ClassQuery.exact,
    SimpleSubclassResult.empty,
  );
  check(B, ClassQuery.exact, C, ClassQuery.subtype, SimpleSubclassResult.empty);
  check(B, ClassQuery.subtype, C, ClassQuery.exact, SimpleSubclassResult.empty);
  check(
    B,
    ClassQuery.subclass,
    C,
    ClassQuery.subclass,
    SimpleSubclassResult.empty,
  );
  check(B, ClassQuery.subclass, C, ClassQuery.subtype, SetSubclassResult([]));
  check(B, ClassQuery.subtype, C, ClassQuery.subclass, SetSubclassResult([G]));
  check(
    B,
    ClassQuery.subtype,
    C,
    ClassQuery.subtype,
    SetSubclassResult([F, G]),
  );
}

testLiveMembers() async {
  final env = await TypeEnvironment.create(r"""
      class A { int a() => 1; }

      mixin B { int b(); }
      class C with B { int b() => 2; }

      mixin D { int d(); }
      mixin E on D { int e() => d(); }
      class F implements D { int d() => 3; }
      class G extends F with E {}

      abstract class H { int h(); }
      class I implements H { int h() => 4; }

      abstract class J { int j(); }

      abstract class K { int k(); }
      class L extends K { int k() => 5; }

      abstract class M { int m(); }
      class N extends M { int m() => 6; }

      main() {
        A().a();
        C().b();
        G().e();
        I().h();
        L().k();
        N();
      }
      """, testBackendWorld: true);

  JClosedWorld closedWorld = env.jClosedWorld;

  void check(
    String clsName,
    String memberName, {
    bool expectExists = true,
    bool expectAbstract = false,
    bool expectConcrete = false,
  }) {
    final cls = env.getClass(clsName);
    final member = closedWorld.elementEnvironment.lookupLocalClassMember(
      cls,
      Name(memberName, Uri()),
    );
    if (expectExists) {
      Expect.isNotNull(
        member,
        "Expected $clsName to contain member $memberName.",
      );
    } else {
      Expect.isNull(
        member,
        "Expected $clsName not to contain member $memberName.",
      );
      return;
    }
    Expect.isTrue(
      expectAbstract ^ expectConcrete,
      "Can only expect to be in one of liveAbstractInstanceMembers and "
      "liveInstanceMembers.",
    );
    if (expectAbstract) {
      Expect.isTrue(
        closedWorld.liveAbstractInstanceMembers.contains(member),
        "Expected $member to exist in liveAbstractInstanceMembers.",
      );
      Expect.isFalse(
        closedWorld.liveInstanceMembers.contains(member),
        "Expected $member to not exist in liveInstanceMembers.",
      );
    } else {
      Expect.isTrue(
        closedWorld.liveInstanceMembers.contains(member),
        "Expected $member to exist in liveInstanceMembers.",
      );
      Expect.isFalse(
        closedWorld.liveAbstractInstanceMembers.contains(member),
        "Expected $member to not exist in liveAbstractInstanceMembers.",
      );
    }
  }

  check('A', 'a', expectConcrete: true);
  check('B', 'b', expectAbstract: true);
  check('C', 'b', expectConcrete: true);
  check('D', 'd', expectAbstract: true);
  check('E', 'e', expectConcrete: true);
  check('F', 'd', expectConcrete: true);
  check('H', 'h', expectAbstract: true);
  check('I', 'h', expectConcrete: true);
  check('J', 'j', expectExists: false);
  check('K', 'k', expectAbstract: true);
  check('L', 'k', expectConcrete: true);
  check('M', 'm', expectExists: false);
  check('N', 'm', expectExists: false);
}
